//
// Created by alfielin on 2021/8/20.
//

#pragma once

#include "spdlog/spdlog.h"
#include "werewolf/constants.cpp"
#include "user_status.cpp"
#include "game_config.cpp"
#include "room_context.cpp"
#include "werewolf/night/handler_chain_builder.cpp"

namespace nz {
namespace werewolf {

class GameRoom : public std::enable_shared_from_this<GameRoom> {
private:
    std::string ownerid;
    int roomid{};
    GameConfig game_config;                            // number of i-th role is v[i]
    std::vector<int> available_role;                   // number of available i-th role is available_role[i]
    RoomContext context;                               // context of the room
    std::shared_ptr<HandlerChain> chain;

    std::mutex assign_role_mutex;       // mutex for assigning the user role

public:

    static std::unordered_map<std::string, int> userid2roomid; // userid lookup table to find its room

    GameRoom () : std::enable_shared_from_this<GameRoom>() {
    }

    GameRoom (int _roomid, const std::string &_ownerid, const GameConfig &_conf)
            : std::enable_shared_from_this<GameRoom>() {
        roomid = _roomid;
        ownerid = _ownerid;
        game_config = _conf;
        available_role = game_config.v;
    }

    bool start_game () {
        if (check_ready_to_start()) {
            chain = HandlerChainBuilder::build(game_config, &context);
            context.init();
            start_night();
            return true;
        }
        return false;
    }

    void start_night () {
        context.clear_night();
        chain->start_handle();
    }

    std::vector<int> take_action (ActionT action, const std::string &userid, const std::vector<std::string> &targets) {
        auto role = RoleT(context.users.at(userid).role);
        switch (action) {
        case ActionT::VOTE:
            context.exile_voter.vote(userid, targets[0]);
            return {};
        case ActionT::SUNSET:
            start_night();
            return {};
        case ActionT::SHOOT:
            context.users.at(targets[0]).kill(context.users.at(userid).have_will, false);
            return {};
        case ActionT::WOLF_SUICIDE:
            context.users.at(userid).kill(false, false);
            start_night();
            return {};
        case ActionT::KING_SUICIDE:
            context.users.at(userid).kill(false, false);
            context.users.at(targets[0]).kill(false, false);
            start_night();
            return {};
        case ActionT::NIGHT_NEXT:
            chain->handle_next(role);
            return {};
        case ActionT::NIGHT_ACT:
            if (!chain->is_in_round(role)) {
                return { -1 };
            }
            switch (role) {
            case RoleT::JUPITER:
                context.set_couple(targets[0], targets[1]);
                return {};
            case RoleT::PROPHET:
                return { context.users.at(targets[0]).is_wolf() };
            case RoleT::WOLF:
                context.wolf_voter.vote(userid, targets[0]);
                return {};
            case RoleT::WITCH:
                if (!targets.empty()) {
                    context.poisoned_tonight = (targets[0] == POISON_TAG);
                    context.night[int(RoleT::WITCH)] = { targets[1] };
                }
                return {};
            case RoleT::GUARDIAN:
                context.night[int(RoleT::GUARDIAN)] = targets;
                return {};
            default:
                SPDLOG_LOGGER_ERROR(LOGGER, "Role {} do not take action at night", ROLE_NAME_MAP.at(int(role)));
                return { -1 };
            }
        default:
            SPDLOG_LOGGER_ERROR(LOGGER, "Should never reach");
        }
        SPDLOG_LOGGER_ERROR(LOGGER, "Should never reach");
        return { -1 }; // should never reach
    }

    int join (const std::string &userid) {
        userid2roomid[userid] = roomid;
        int role, index;
        { // ======================== Critical Area ========================
#ifndef NZDEBUG_WEREWOLF
            std::unique_lock<std::mutex> lck(assign_role_mutex);
            RNG rng;
            role = rng.rand_int(0, available_role.size(), [this](int _role) {
                return available_role[_role] != 0;
            });
#else       // Disable random in debug mode
            for (int i = 0; i < available_role.size(); ++i) {
                if (available_role[i] != 0) {
                    role = i;
                }
            }
#endif
            index = int(context.users.size());
            context.users[userid] = UserStatus(userid, index, role);
            available_role[role] -= 1;
        }
        LOGGER->debug("User {} joined room {} with role {}", userid, roomid, ROLE_NAME_MAP.at(role));
        return role;
    }

    void exit (const std::string &userid) {
        userid2roomid.erase(userid);
        int role = context.users.at(userid).role;
        available_role[role] += 1;
        context.users.erase(userid);
    }

    ~GameRoom () {
        LOGGER->info("Room {} destroyed", roomid);
    }

private:
    bool check_ready_to_start () {
        return context.users.size() == game_config.required_users_num();
    }

public: // getter and setters

    const std::string &get_ownerid () const {
        return ownerid;
    }

    int get_roomid () const {
        return roomid;
    }

    static int get_roomid_by_userid (const std::string &userid) {
        if (!userid2roomid.count(userid)) {
            return -1;
        }
        return userid2roomid.at(userid);
    }

    const GameConfig &get_role_config () const { return game_config; }

    void set_role_config (const GameConfig &new_game_config) {
        game_config = new_game_config;
        available_role = new_game_config.v;
    }

    std::vector<int> get_available_role () { return available_role; }

    const UserStatus &get_user_status (const std::string &userid) const {
        return context.users.at(userid);
    }

    std::vector<std::string> get_night_death_list () const {
        return context.get_night_death_list();
    }

};

std::unordered_map<std::string, int> GameRoom::userid2roomid = {};

}
}

